a tool for shared writing and social publishing
1import { subscribeToPublication } from "app/lish/subscribeToPublication";
2import { cookies } from "next/headers";
3import { redirect } from "next/navigation";
4import { NextRequest, NextResponse } from "next/server";
5import { createOauthClient } from "src/atproto-oauth";
6import { setAuthToken } from "src/auth";
7
8import { supabaseServerClient } from "supabase/serverClient";
9import { URLSearchParams } from "url";
10import {
11 ActionAfterSignIn,
12 parseActionFromSearchParam,
13} from "./afterSignInActions";
14import { inngest } from "app/api/inngest/client";
15
16type OauthRequestClientState = {
17 redirect: string | null;
18 action: ActionAfterSignIn | null;
19};
20
21export async function GET(
22 req: NextRequest,
23 props: { params: Promise<{ route: string; handle?: string }> },
24) {
25 const params = await props.params;
26 let client = await createOauthClient();
27 switch (params.route) {
28 case "metadata":
29 return NextResponse.json(client.clientMetadata);
30 case "jwks":
31 return NextResponse.json(client.jwks);
32 case "login": {
33 const searchParams = req.nextUrl.searchParams;
34 const handle = searchParams.get("handle") as string;
35 // Put originating page here!
36 let redirect = searchParams.get("redirect_url");
37 if (redirect) redirect = decodeURIComponent(redirect);
38 let action = parseActionFromSearchParam(searchParams.get("action"));
39 let state: OauthRequestClientState = { redirect, action };
40
41 // Revoke any pending authentication requests if the connection is closed (optional)
42 const ac = new AbortController();
43
44 const url = await client.authorize(handle || "https://bsky.social", {
45 scope: "atproto transition:generic transition:email",
46 signal: ac.signal,
47 state: JSON.stringify(state),
48 });
49
50 return NextResponse.redirect(url);
51 }
52 case "callback": {
53 const params = new URLSearchParams(req.url.split("?")[1]);
54
55 let redirectPath = "/";
56 try {
57 const { session, state } = await client.callback(params);
58 let s: OauthRequestClientState = JSON.parse(state || "{}");
59 redirectPath = decodeURIComponent(s.redirect || "/");
60 let { data: identity } = await supabaseServerClient
61 .from("identities")
62 .select()
63 .eq("atp_did", session.did)
64 .single();
65 if (!identity) {
66 let existingIdentity = (await cookies()).get("auth_token");
67 if (existingIdentity) {
68 let data = await supabaseServerClient
69 .from("email_auth_tokens")
70 .select("*, identities(*)")
71 .eq("id", existingIdentity.value)
72 .single();
73 if (data.data?.identity && data.data.confirmed)
74 await supabaseServerClient
75 .from("identities")
76 .update({ atp_did: session.did })
77 .eq("id", data.data.identity);
78
79 return handleAction(s.action, redirectPath);
80 }
81 const { data } = await supabaseServerClient
82 .from("identities")
83 .insert({ atp_did: session.did })
84 .select()
85 .single();
86 identity = data;
87 }
88
89 // Trigger migration if identity needs it
90 const metadata = identity?.metadata as Record<string, unknown> | null;
91 if (metadata?.needsStandardSiteMigration) {
92 await inngest.send({
93 name: "user/migrate-to-standard",
94 data: { did: session.did },
95 });
96 }
97
98 let { data: token } = await supabaseServerClient
99 .from("email_auth_tokens")
100 .insert({
101 identity: identity!.id,
102 confirmed: true,
103 confirmation_code: "",
104 })
105 .select()
106 .single();
107
108 if (token) await setAuthToken(token.id);
109
110 // Process successful authentication here
111 console.log("authorize() was called with state:", state);
112
113 console.log("User authenticated as:", session.did);
114 return handleAction(s.action, redirectPath);
115 } catch (e) {
116 redirect(redirectPath);
117 }
118 }
119 default:
120 return NextResponse.json({ error: "Invalid route" }, { status: 404 });
121 }
122}
123
124const handleAction = async (
125 action: ActionAfterSignIn | null,
126 redirectPath: string,
127) => {
128 let parsePath = decodeURIComponent(redirectPath);
129 let url;
130 if (parsePath.includes("://")) url = new URL(parsePath);
131 else url = new URL(decodeURIComponent(redirectPath), "https://example.com");
132 if (action?.action === "subscribe") {
133 let result = await subscribeToPublication(action.publication);
134 if (result.success && result.hasFeed === false)
135 url.searchParams.set("showSubscribeSuccess", "true");
136 }
137
138 let path = url.pathname;
139 if (url.search) path += url.search;
140 if (url.hash) path += url.hash;
141 return parsePath.includes("://") ? redirect(url.toString()) : redirect(path);
142};